/*
 * Copyright 2002-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.jdbc.core;

import java.lang.reflect.Constructor;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.TypeConverter;
import org.springframework.core.convert.ConversionService;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

/**
 * {@link RowMapper} implementation that converts a row into a new instance
 * of the specified mapped target class. The mapped target class must be a
 * top-level class and may either expose a data class constructor with named
 * parameters corresponding to column names or classic bean property setters
 * (or even a combination of both).
 *
 * <p>Note that this class extends {@link BeanPropertyRowMapper} and can
 * therefore serve as a common choice for any mapped target class, flexibly
 * adapting to constructor style versus setter methods in the mapped class.
 *
 * @param <T> the result type
 * @author Juergen Hoeller
 * @since 5.3
 */
public class DataClassRowMapper<T> extends BeanPropertyRowMapper<T> {

    @Nullable
    private Constructor<T> mappedConstructor;

    @Nullable
    private String[] constructorParameterNames;

    @Nullable
    private Class<?>[] constructorParameterTypes;


    /**
     * Create a new {@code DataClassRowMapper} for bean-style configuration.
     *
     * @see #setMappedClass
     * @see #setConversionService
     */
    public DataClassRowMapper() {
    }

    /**
     * Create a new {@code DataClassRowMapper}.
     *
     * @param mappedClass the class that each row should be mapped to
     */
    public DataClassRowMapper(Class<T> mappedClass) {
        super(mappedClass);
    }


    @Override
    protected void initialize(Class<T> mappedClass) {
        super.initialize(mappedClass);

        this.mappedConstructor = BeanUtils.getResolvableConstructor(mappedClass);
        if (this.mappedConstructor.getParameterCount() > 0) {
            this.constructorParameterNames = BeanUtils.getParameterNames(this.mappedConstructor);
            this.constructorParameterTypes = this.mappedConstructor.getParameterTypes();
        }
    }

    @Override
    protected T constructMappedInstance(ResultSet rs, TypeConverter tc) throws SQLException {
        Assert.state(this.mappedConstructor != null, "Mapped constructor was not initialized");

        Object[] args;
        if (this.constructorParameterNames != null && this.constructorParameterTypes != null) {
            args = new Object[this.constructorParameterNames.length];
            for (int i = 0; i < args.length; i++) {
                String name = underscoreName(this.constructorParameterNames[i]);
                Class<?> type = this.constructorParameterTypes[i];
                args[i] = tc.convertIfNecessary(getColumnValue(rs, rs.findColumn(name), type), type);
            }
        } else {
            args = new Object[0];
        }

        return BeanUtils.instantiateClass(this.mappedConstructor, args);
    }


    /**
     * Static factory method to create a new {@code DataClassRowMapper}.
     *
     * @param mappedClass the class that each row should be mapped to
     * @see #newInstance(Class, ConversionService)
     */
    public static <T> DataClassRowMapper<T> newInstance(Class<T> mappedClass) {
        return new DataClassRowMapper<>(mappedClass);
    }

    /**
     * Static factory method to create a new {@code DataClassRowMapper}.
     *
     * @param mappedClass       the class that each row should be mapped to
     * @param conversionService the {@link ConversionService} for binding
     *                          JDBC values to bean properties, or {@code null} for none
     * @see #newInstance(Class)
     * @see #setConversionService
     */
    public static <T> DataClassRowMapper<T> newInstance(
            Class<T> mappedClass, @Nullable ConversionService conversionService) {

        DataClassRowMapper<T> rowMapper = newInstance(mappedClass);
        rowMapper.setConversionService(conversionService);
        return rowMapper;
    }

}
